﻿// The Nova Project by Ken Beckett.
// Copyright (C) 2007-2012 Inevitable Software, all rights reserved.
// Released under the Common Development and Distribution License, CDDL-1.0: http://opensource.org/licenses/cddl1.php

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Windows.Media;

using Nova.CodeDOM;
using Nova.Utilities;

namespace Nova.UI
{
    /// <summary>
    /// The view model for a <see cref="CodeDOM.TypeRefBase"/>.
    /// </summary>
    public abstract class TypeRefBaseVM : SymbolicRefVM
    {
        #region /* FIELDS */

        /// <summary>
        /// Optional type arguments (for generic types only).
        /// </summary>
        protected ChildListVM<ExpressionVM> _typeArgumentVMs;

        #endregion

        #region /* CONSTRUCTORS */

        /// <summary>
        /// Create a view model instance for the specified <see cref="CodeDOM.TypeRefBase"/>.
        /// </summary>
        protected TypeRefBaseVM(TypeRefBase typeRefBase, bool createTypeArgumentVMs, Dictionary<CodeObject, CodeObjectVM> dictionary)
            : base(typeRefBase, dictionary)
        {
            if (createTypeArgumentVMs)
                _typeArgumentVMs = CreateListVM<Expression, ExpressionVM>(typeRefBase.TypeArguments, dictionary);
        }

        #endregion

        #region /* PROPERTIES */

        /// <summary>
        /// The underlying <see cref="CodeDOM.TypeRefBase"/> model.
        /// </summary>
        public TypeRefBase TypeRefBase
        {
            get { return (TypeRefBase)CodeObject; }
        }

        /// <summary>
        /// The type argument <see cref="Expression"/>s of the reference (if any).
        /// </summary>
        public virtual ChildListVM<ExpressionVM> TypeArguments
        {
            get { return _typeArgumentVMs; }
        }

        /// <summary>
        /// True if there are any type arguments.
        /// </summary>
        public bool HasTypeArguments
        {
            get { return (TypeArguments != null && TypeArguments.Count > 0); }
        }

        /// <summary>
        /// The number of type arguments.
        /// </summary>
        public int TypeArgumentCount
        {
            get { return (TypeArguments != null ? TypeArguments.Count : 0); }
        }

        #endregion

        #region /* METHODS */

        #endregion

        #region /* RENDERING */

        public override void RenderToolTip(CodeRenderer renderer, RenderFlags flags)
        {
            RenderType(renderer, CodeObject.GetType(), flags, this);
            renderer.RenderRightArrow(NORMAL_BRUSH, this);
            RenderReferenceDescription(renderer, TypeRefBase.GetTypeWithoutConstant().Reference);
            RenderSummaryCommentInToolTip(renderer);
            RenderMessagesInToolTip(renderer, flags);
        }

        /// <summary>
        /// Render a <see cref="Type"/>.
        /// </summary>
        public static void RenderType(CodeRenderer renderer, Type type, RenderFlags flags, CodeObjectVM tag)
        {
            RenderFlags passFlags = flags & ~RenderFlags.Description;

            // Dereference (remove the trailing '&' or '*') if it's a reference type or pointer type
            if (type.IsByRef)
                type = type.GetElementType();

            bool hasBorder = false;
            bool isDelegate = false;
            bool isDescription = flags.HasFlag(RenderFlags.Description);
            if (isDescription)
            {
                string declType = null;
                Brush borderBrush = DarkPalerGreen;
                Brush backgroundBrush = PalerGreen;
                if (type.IsClass)
                {
                    if (TypeUtil.IsDelegateType(type))
                    {
                        declType = DelegateDecl.ParseToken;
                        borderBrush = DelegateDeclVM.StaticBorderBrush;
                        backgroundBrush = DelegateDeclVM.StaticBackgroundBrush;
                        isDelegate = true;
                    }
                    else if (!type.IsGenericParameter)
                    {
                        declType = ClassDecl.ParseToken;
                        borderBrush = ClassDeclVM.StaticBorderBrush;
                        backgroundBrush = ClassDeclVM.StaticBackgroundBrush;
                    }
                }
                else if (type.IsInterface)
                {
                    declType = InterfaceDecl.ParseToken;
                    borderBrush = InterfaceDeclVM.StaticBorderBrush;
                    backgroundBrush = InterfaceDeclVM.StaticBackgroundBrush;
                }
                else if (type.IsEnum)
                {
                    declType = EnumDecl.ParseToken;
                    borderBrush = EnumDeclVM.StaticBorderBrush;
                    backgroundBrush = EnumDeclVM.StaticBackgroundBrush;
                }
                else if (type.IsValueType)
                {
                    declType = StructDecl.ParseToken;
                    borderBrush = StructDeclVM.StaticBorderBrush;
                    backgroundBrush = StructDeclVM.StaticBackgroundBrush;
                }

                hasBorder = (flags.HasFlag(RenderFlags.ForceBorder) && !flags.HasFlag(RenderFlags.NoBorder) && !type.IsGenericParameter);
                if (hasBorder)
                    renderer.CreateBorder(borderBrush, backgroundBrush, tag);

                MethodInfo delegateInvokeMethodInfo = (isDelegate ? TypeUtil.GetInvokeMethod(type) : null);
                if (!flags.HasFlag(RenderFlags.NoPreAnnotations))
                {
                    AttributeVM.RenderAttributes(renderer, type, tag);
                    if (delegateInvokeMethodInfo != null)
                        AttributeVM.RenderAttributes(renderer, delegateInvokeMethodInfo.ReturnParameter, tag, AttributeTarget.Return);
                }
                if (type.IsGenericParameter)
                {
                    renderer.RenderText("(type parameter) ", NORMAL_BRUSH, TextStyle.Proportional);
                    if (type.DeclaringMethod != null)
                        RenderGenericMember(renderer, type.DeclaringMethod.Name, type.DeclaringMethod.GetGenericArguments(), passFlags, TYPE_BRUSH, tag);
                    else if (type.DeclaringType != null)
                        RenderTypeAsTypeRefVM(renderer, type.DeclaringType, passFlags, tag);  // Render as TypeRefVM for nested tooltips
                    renderer.RenderText(Dot.ParseToken, PUNC_BRUSH, tag);
                }
                else
                {
                    renderer.RenderText(ModifiersHelpers.AsString(TypeRefBase.GetTypeModifiers(type)), KEYWORD_BRUSH, tag);
                    renderer.RenderText(declType, KEYWORD_BRUSH, tag);
                    renderer.RenderText(" ", PUNC_BRUSH, tag);
                }
                if (delegateInvokeMethodInfo != null)
                {
                    Type returnType = delegateInvokeMethodInfo.ReturnType;
                    RenderTypeAsTypeRefVM(renderer, returnType, RenderFlags.None, tag);  // Render as TypeRefVM for nested tooltips
                    renderer.RenderText(" ", PUNC_BRUSH, tag);
                }
            }

            bool hasDotPrefix = flags.HasFlag(RenderFlags.HasDotPrefix);

            // Go by local generic arguments instead of IsGenericType so that nested types with generic
            // enclosing types aren't treated as generic unless they have their own type arguments.
            string keyword;
            Type[] genericArguments = TypeUtil.GetLocalGenericArguments(type);
            if (genericArguments != null && genericArguments.Length > 0)
            {
                // Render "Nullable<Type>" as "Type?"
                if (TypeUtil.IsNullableType(type))
                {
                    Type typeParam = genericArguments[0];
                    if (!hasDotPrefix && TypeRefBase.TypeNameToKeywordMap.TryGetValue(typeParam.Name, out keyword) && typeParam.Namespace == "System")
                        renderer.RenderText(keyword + TypeRefBase.ParseTokenNullable, KEYWORD_BRUSH, tag);
                    else
                    {
                        RenderTypeAsTypeRefVM(renderer, typeParam, RenderFlags.None, tag);  // Render as TypeRefVM for nested tooltips
                        renderer.RenderText(TypeRefBase.ParseTokenNullable, TYPE_BRUSH, tag);
                    }
                }
                else
                {
                    if (type.IsNested && !hasDotPrefix && flags.HasFlag(RenderFlags.ShowParentTypes))
                    {
                        RenderTypeAsTypeRefVM(renderer, type.DeclaringType, passFlags, tag);  // Render as TypeRefVM for nested tooltips
                        renderer.RenderText(Dot.ParseToken, PUNC_BRUSH, tag);
                    }
                    RenderGenericMember(renderer, TypeUtil.NonGenericName(type), genericArguments, flags, TYPE_BRUSH, tag);
                }
            }
            else if (type.IsGenericParameter)
            {
                // If there is an 'open' type parameter, render it as an error (or warning if in a doc comment)
                Brush brush = (tag is OpenTypeParameterRefVM ? (flags.HasFlag(RenderFlags.InDocComment) ? WARNING_BRUSH : ERROR_BRUSH) : IDENTIFIER_BRUSH);
                renderer.RenderText(type.Name, brush, tag);
            }
            else if (type.IsArray)
            {
                RenderTypeAsTypeRefVM(renderer, type.GetElementType(), RenderFlags.None, tag);  // Render as TypeRefVM for nested tooltips
                renderer.RenderText(TypeRefBase.ArrayRankToString(type.GetArrayRank()), PUNC_BRUSH, tag);
            }
            else if (!hasDotPrefix && !isDescription && TypeRefBase.TypeNameToKeywordMap.TryGetValue(type.Name, out keyword) && type.Namespace == "System")
            {
                renderer.RenderText(keyword, KEYWORD_BRUSH, tag);
            }
            else
            {
                if (type.IsNested && !hasDotPrefix && flags.HasFlag(RenderFlags.ShowParentTypes))
                {
                    RenderTypeAsTypeRefVM(renderer, type.DeclaringType, passFlags, tag);  // Render as TypeRefVM for nested tooltips
                    renderer.RenderText(Dot.ParseToken, PUNC_BRUSH, tag);
                }
                renderer.RenderName(type.Name, TYPE_BRUSH, tag, flags);
            }

            if (isDescription)
            {
                if (isDelegate)
                {
                    MethodInfo delegateInvokeMethodInfo = TypeUtil.GetInvokeMethod(type);
                    if (delegateInvokeMethodInfo != null)
                        MethodRefVM.RenderMethodParameters(renderer, delegateInvokeMethodInfo, RenderFlags.None, tag);
                }
                else
                {
                    bool hasBaseItem = false;

                    // Render base type (if any)
                    if (!type.IsValueType)  // Don't show the implicit ValueType base for structs
                    {
                        Type baseType = type.IsEnum ? Enum.GetUnderlyingType(type) : type.BaseType;
                        if (baseType != null && baseType != typeof(object) && (!type.IsEnum || baseType != typeof(int)))
                        {
                            renderer.RenderText(" " + BaseListTypeDecl.ParseToken + " ", PUNC_BRUSH, tag);
                            RenderTypeAsTypeRefVM(renderer, baseType, flags, tag);  // Render as TypeRefVM for nested tooltips
                            hasBaseItem = true;
                        }
                    }

                    // Render implemented interfaces (if any)
                    if (!type.IsEnum)  // Don't show the interfaces of the implicit Enum base for enums
                    {
                        Type[] interfaces = type.GetInterfaces();
                        if (interfaces.Length > 0)
                        {
                            // There's no way to tell which interfaces are implemented by the current type as
                            // opposed to one of it's base types.  We could subtract all interfaces from the base
                            // type, but that wouldn't be perfect since the same interface can be implemented at
                            // multiple levels.  Instead, display them all, since an external user really just
                            // cares that they're implemented by the type, and doesn't care if it's really by a
                            // base type.
                            foreach (Type @interface in interfaces)
                            {
                                renderer.RenderText((hasBaseItem ? Expression.ParseTokenSeparator : (" " + BaseListTypeDecl.ParseToken)) + " ", PUNC_BRUSH, tag);
                                RenderTypeAsTypeRefVM(renderer, @interface, flags, tag);  // Render as TypeRefVM for nested tooltips
                                hasBaseItem = true;
                            }
                        }
                    }

                    // Render any type constraints for local type arguments
                    if (type.IsGenericType)
                        RenderConstraints(renderer, TypeUtil.GetLocalGenericArguments(type.GetGenericTypeDefinition()), passFlags, tag);
                }
            }

            // If we have a border, return to the previous one
            if (hasBorder)
                renderer.ReturnToBorderParent(tag);
        }

        /// <summary>
        /// Render a Type as a TypeRefVM for nested tooltips.
        /// </summary>
        public static void RenderTypeAsTypeRefVM(CodeRenderer renderer, Type type, RenderFlags flags, CodeObjectVM parentVM)
        {
            // Treat generic parameters as TypeParameter definitions
            CodeObject codeObject;
            if (type.IsGenericParameter)
                codeObject = new TypeParameter(type.Name);
            else
            {
                codeObject = TypeRef.Create(type);
                // Evaluate type parameters some day, but also need to evaluate the type arguments in the declaration
                //TypeRefBase typeRefBase = TypeRef.Create(typeReference).EvaluateTypeArgumentTypes(parentVM.CodeObject);
            }

            // Use parentVM to render if possible, but it can be null if we're inside a tooltip
            CodeObjectVM codeObjectVM;
            if (parentVM != null)
                codeObjectVM = parentVM.CreateVM(codeObject, true);
            else
                codeObjectVM = CreateVM(codeObject, null, true);
            codeObjectVM.Render(renderer, flags | RenderFlags.NoBorder);
        }

        /// <summary>
        /// Render a generic member.
        /// </summary>
        public static void RenderGenericMember(CodeRenderer renderer, string name, Type[] args, RenderFlags flags, Brush brush, CodeObjectVM tag)
        {
            renderer.RenderText(name, brush, tag);
            if (!flags.HasFlag(RenderFlags.SuppressTypeArgs))
            {
                // Render the angle brackets normally in the GUI, even if we're inside a documentation comment
                renderer.RenderText(TypeRefBase.ParseTokenArgumentStart, PUNC_BRUSH, tag);
                bool first = true;
                foreach (Type typeArg in args)
                {
                    if (!first)
                        renderer.RenderText(Expression.ParseTokenSeparator + " ", PUNC_BRUSH, tag);
                    RenderTypeAsTypeRefVM(renderer, typeArg, RenderFlags.None, tag);  // Render as TypeRefVM for nested tooltips
                    first = false;
                }
                renderer.RenderText(TypeRefBase.ParseTokenArgumentEnd, PUNC_BRUSH, tag);
            }
        }

        /// <summary>
        /// Render type arguments (if any).
        /// </summary>
        public static void RenderTypeArguments(CodeRenderer renderer, IEnumerable<ExpressionVM> typeArguments, RenderFlags flags, CodeObjectVM tag)
        {
            if (typeArguments != null)
            {
                // Don't pass Description, SuppressBrackets, or ShowParentTypes on to any type arguments, and don't display EOL comments if this is a Description
                RenderFlags passFlags = (flags & (RenderFlags.PassMask & ~(RenderFlags.Description | RenderFlags.SuppressBrackets | RenderFlags.ShowParentTypes)))
                    | (flags.HasFlag(RenderFlags.Description) ? RenderFlags.NoEOLComments : 0);

                // Render the angle brackets normally in the GUI, even if we're inside a documentation comment
                renderer.RenderText(TypeRefBase.ParseTokenArgumentStart, PUNC_BRUSH, tag);
                renderer.RenderList(typeArguments, passFlags, tag);
                renderer.RenderText(TypeRefBase.ParseTokenArgumentEnd, PUNC_BRUSH, tag);
            }
        }

        /// <summary>
        /// Render array rank brackets (if any).
        /// </summary>
        public void RenderArrayRanks(CodeRenderer renderer, RenderFlags flags)
        {
            if (!flags.HasFlag(RenderFlags.SuppressBrackets))
            {
                // Use the array ranks if we have any, otherwise use any on the referenced type
                if (TypeRefBase.ArrayRanks != null)
                {
                    foreach (int rank in TypeRefBase.ArrayRanks)
                    {
                        // Render with a null object tag for now (no special tooltip handling)
                        renderer.RenderText(TypeRefBase.ArrayRankToString(rank), PUNC_BRUSH, null);
                    }
                }
                else if (TypeRefBase.Reference is Type)
                    RenderArrayRank(renderer, (Type)TypeRefBase.Reference);
            }
        }

        private static void RenderArrayRank(CodeRenderer renderer, Type type)
        {
            if (type.IsArray)
            {
                RenderArrayRank(renderer, type.GetElementType());
                // Render with a null object tag for now (no special tooltip handling)
                renderer.RenderText(TypeRefBase.ArrayRankToString(type.GetArrayRank()), PUNC_BRUSH, null);
            }
        }

        /// <summary>
        /// Render type constraints.
        /// </summary>
        public static void RenderConstraints(CodeRenderer renderer, Type[] typeParameters, RenderFlags flags, CodeObjectVM tag)
        {
            foreach (Type typeParameter in typeParameters)
            {
                bool isValueType = false;
                GenericParameterAttributes attributes = typeParameter.GenericParameterAttributes;
                bool hasBorder = false;

                if (attributes.HasFlag(GenericParameterAttributes.ReferenceTypeConstraint))
                {
                    RenderConstraintPrefix(renderer, ref hasBorder, typeParameter, flags, tag);
                    renderer.RenderText(ClassConstraint.ParseToken, KEYWORD_BRUSH, tag);
                }
                if (attributes.HasFlag(GenericParameterAttributes.NotNullableValueTypeConstraint))
                {
                    RenderConstraintPrefix(renderer, ref hasBorder, typeParameter, flags, tag);
                    renderer.RenderText(StructConstraint.ParseToken, KEYWORD_BRUSH, tag);
                    isValueType = true;
                }

                foreach (Type constraint in typeParameter.GetGenericParameterConstraints())
                {
                    if (constraint != typeof(ValueType))  // Already handled above
                    {
                        RenderConstraintPrefix(renderer, ref hasBorder, typeParameter, flags, tag);
                        RenderTypeAsTypeRefVM(renderer, constraint, RenderFlags.None, tag);  // Render as TypeRefVM for nested tooltips
                    }
                }

                if (attributes.HasFlag(GenericParameterAttributes.DefaultConstructorConstraint) && !isValueType)
                {
                    RenderConstraintPrefix(renderer, ref hasBorder, typeParameter, flags, tag);
                    renderer.RenderText(NewConstraint.ParseToken, KEYWORD_BRUSH, tag);
                    if (!MethodDeclBaseVM.HideMethodParens)
                        renderer.RenderText(Expression.ParseTokenStartGroup + Expression.ParseTokenEndGroup, PUNC_BRUSH, tag);
                }

                // If we have a border, return to the previous one
                if (hasBorder)
                    renderer.ReturnToBorderParent(tag);
            }
        }

        private static void RenderConstraintPrefix(CodeRenderer renderer, ref bool hasBorder, Type typeParameter, RenderFlags flags, CodeObjectVM tag)
        {
            if (!hasBorder)
            {
                renderer.RenderText(" ", PUNC_BRUSH, tag);
                hasBorder = !flags.HasFlag(RenderFlags.NoBorder);
                if (hasBorder)
                     renderer.CreateBorder(StaticBorderBrush, StaticBackgroundBrush, tag);

                renderer.RenderText(ConstraintClause.ParseToken + " ", KEYWORD_BRUSH, tag);
                RenderType(renderer, typeParameter, RenderFlags.None, tag);
                renderer.RenderText(" " + ConstraintClause.ParseTokenSeparator + " ", PUNC_BRUSH, tag);
            }
            else
                renderer.RenderText(Expression.ParseTokenSeparator + " ", PUNC_BRUSH, tag);
        }

        public override void UnRender()
        {
            ChildListHelpers.UnRender(_typeArgumentVMs);
            base.UnRender();
        }

        #endregion
    }
}
